(function ($, undef) {
    if ($.fn.dotdotdot) {
        return;
    }

    $.fn.dotdotdot = function (o) {
        if (this.length == 0) {
            $.fn.dotdotdot.debug('No element found for "' + this.selector + '".');
            return this;
        }
        if (this.length > 1) {
            return this.each(
                function () {
                    $(this).dotdotdot(o);
                }
            );
        }


        var $dot = this;
        var orgContent = $dot.contents();

        if ($dot.data('dotdotdot')) {
            $dot.trigger('destroy.dot');
        }

        $dot.data('dotdotdot-style', $dot.attr('style') || '');
        $dot.css('word-wrap', 'break-word');
        if ($dot.css('white-space') === 'nowrap') {
            $dot.css('white-space', 'normal');
        }

        $dot.bind_events = function () {
            $dot.bind(
                'update.dot',
                function (e, c) {
                    $dot.removeClass("is-truncated");
                    e.preventDefault();
                    e.stopPropagation();

                    switch (typeof opts.height) {
                        case 'number':
                            opts.maxHeight = opts.height;
                            break;

                        case 'function':
                            opts.maxHeight = opts.height.call($dot[0]);
                            break;

                        default:
                            opts.maxHeight = getTrueInnerHeight($dot);
                            break;
                    }

                    opts.maxHeight += opts.tolerance;

                    if (typeof c != 'undefined') {
                        if (typeof c == 'string' || ('nodeType' in c && c.nodeType === 1)) {
                            c = $('<div />').append(c).contents();
                        }
                        if (c instanceof $) {
                            orgContent = c;
                        }
                    }

                    $inr = $dot.wrapInner('<div class="dotdotdot" />').children();
                    $inr.contents()
                        .detach()
                        .end()
                        .append(orgContent.clone(true))
                        .find('br')
                        .replaceWith('  <br />  ')
                        .end()
                        .css({
                            'height': 'auto',
                            'width': 'auto',
                            'border': 'none',
                            'padding': 0,
                            'margin': 0
                        });

                    var after = false,
                        trunc = false;

                    if (conf.afterElement) {
                        after = conf.afterElement.clone(true);
                        after.show();
                        conf.afterElement.detach();
                    }

                    if (test($inr, opts)) {
                        if (opts.wrap == 'children') {
                            trunc = children($inr, opts, after);
                        } else {
                            trunc = ellipsis($inr, $dot, $inr, opts, after);
                        }
                    }
                    $inr.replaceWith($inr.contents());
                    $inr = null;

                    if ($.isFunction(opts.callback)) {
                        opts.callback.call($dot[0], trunc, orgContent);
                    }

                    conf.isTruncated = trunc;
                    return trunc;
                }
            ).bind(
                'isTruncated.dot',
                function (e, fn) {
                    e.preventDefault();
                    e.stopPropagation();

                    if (typeof fn == 'function') {
                        fn.call($dot[0], conf.isTruncated);
                    }
                    return conf.isTruncated;
                }
            ).bind(
                'originalContent.dot',
                function (e, fn) {
                    e.preventDefault();
                    e.stopPropagation();

                    if (typeof fn == 'function') {
                        fn.call($dot[0], orgContent);
                    }
                    return orgContent;
                }
            ).bind(
                'destroy.dot',
                function (e) {
                    e.preventDefault();
                    e.stopPropagation();

                    $dot.unwatch()
                        .unbind_events()
                        .contents()
                        .detach()
                        .end()
                        .append(orgContent)
                        .attr('style', $dot.data('dotdotdot-style') || '')
                        .removeClass('is-truncated')
                        .data('dotdotdot', false);
                }
            );
            return $dot;
        };	//	/bind_events

        $dot.unbind_events = function () {
            $dot.unbind('.dot');
            return $dot;
        };	//	/unbind_events

        $dot.watch = function () {
            $dot.unwatch();
            if (opts.watch == 'window') {
                var $window = $(window),
                    _wWidth = $window.width(),
                    _wHeight = $window.height();

                $window.bind(
                    'resize.dot' + conf.dotId,
                    function () {
                        if (_wWidth != $window.width() || _wHeight != $window.height() || !opts.windowResizeFix) {
                            _wWidth = $window.width();
                            _wHeight = $window.height();

                            if (watchInt) {
                                clearInterval(watchInt);
                            }
                            watchInt = setTimeout(
                                function () {
                                    $dot.trigger('update.dot');
                                }, 100
                            );
                        }
                    }
                );
            } else {
                watchOrg = getSizes($dot);
                watchInt = setInterval(
                    function () {
                        if ($dot.is(':visible')) {
                            var watchNew = getSizes($dot);
                            if (watchOrg.width != watchNew.width ||
                                watchOrg.height != watchNew.height) {
                                $dot.trigger('update.dot');
                                watchOrg = watchNew;
                            }
                        }
                    }, 500
                );
            }
            return $dot;
        };
        $dot.unwatch = function () {
            $(window).unbind('resize.dot' + conf.dotId);
            if (watchInt) {
                clearInterval(watchInt);
            }
            return $dot;
        };

        var opts = $.extend(true, {}, $.fn.dotdotdot.defaults, o),
            conf = {},
            watchOrg = {},
            watchInt = null,
            $inr = null;


        if (!(opts.lastCharacter.remove instanceof Array)) {
            opts.lastCharacter.remove = $.fn.dotdotdot.defaultArrays.lastCharacter.remove;
        }
        if (!(opts.lastCharacter.noEllipsis instanceof Array)) {
            opts.lastCharacter.noEllipsis = $.fn.dotdotdot.defaultArrays.lastCharacter.noEllipsis;
        }


        conf.afterElement = getElement(opts.after, $dot);
        conf.isTruncated = false;
        conf.dotId = dotId++;


        $dot.data('dotdotdot', true)
            .bind_events()
            .trigger('update.dot');

        if (opts.watch) {
            $dot.watch();
        }

        return $dot;
    };


    //	public
    $.fn.dotdotdot.defaults = {
        'ellipsis': '... ',
        'wrap': 'word',
        'fallbackToLetter': true,
        'lastCharacter': {},
        'tolerance': 0,
        'callback': null,
        'after': null,
        'height': null,
        'watch': false,
        'windowResizeFix': true
    };
    $.fn.dotdotdot.defaultArrays = {
        'lastCharacter': {
            'remove': [' ', '\u3000', ',', ';', '.', '!', '?'],
            'noEllipsis': []
        }
    };
    $.fn.dotdotdot.debug = function (msg) {
    };


    //	private
    var dotId = 1;

    function children($elem, o, after) {
        var $elements = $elem.children(),
            isTruncated = false;

        $elem.empty();

        for (var a = 0, l = $elements.length; a < l; a++) {
            var $e = $elements.eq(a);
            $elem.append($e);
            if (after) {
                $elem.append(after);
            }
            if (test($elem, o)) {
                $e.remove();
                isTruncated = true;
                break;
            } else {
                if (after) {
                    after.detach();
                }
            }
        }
        return isTruncated;
    }

    function ellipsis($elem, $d, $i, o, after) {
        var isTruncated = false;

        //	Don't put the ellipsis directly inside these elements
        var notx = 'a, table, thead, tbody, tfoot, tr, col, colgroup, object, embed, param, ol, ul, dl, blockquote, select, optgroup, option, textarea, script, style';

        //	Don't remove these elements even if they are after the ellipsis
        var noty = 'script, .dotdotdot-keep';

        $elem
            .contents()
            .detach()
            .each(
                function () {

                    var e = this,
                        $e = $(e);

                    if (typeof e == 'undefined') {
                        return true;
                    } else if ($e.is(noty)) {
                        $elem.append($e);
                    } else if (isTruncated) {
                        return true;
                    } else {
                        $elem.append($e);
                        if (after && !$e.is(o.after) && !$e.find(o.after).length) {
                            $elem[$elem.is(notx) ? 'after' : 'append'](after);
                        }
                        if (test($i, o)) {
                            if (e.nodeType == 3) // node is TEXT
                            {
                                isTruncated = ellipsisElement($e, $d, $i, o, after);
                            } else {
                                isTruncated = ellipsis($e, $d, $i, o, after);
                            }
                        }

                        if (!isTruncated) {
                            if (after) {
                                after.detach();
                            }
                        }
                    }
                }
            );
        $d.addClass("is-truncated");
        return isTruncated;
    }

    function ellipsisElement($e, $d, $i, o, after) {
        var e = $e[0];

        if (!e) {
            return false;
        }

        var txt = getTextContent(e),
            space = (txt.indexOf(' ') !== -1) ? ' ' : '\u3000',
            separator = (o.wrap == 'letter') ? '' : space,
            textArr = txt.split(separator),
            position = -1,
            midPos = -1,
            startPos = 0,
            endPos = textArr.length - 1;


        //	Only one word
        if (o.fallbackToLetter && startPos == 0 && endPos == 0) {
            separator = '';
            textArr = txt.split(separator);
            endPos = textArr.length - 1;
        }

        while (startPos <= endPos && !(startPos == 0 && endPos == 0)) {
            var m = Math.floor((startPos + endPos) / 2);
            if (m == midPos) {
                break;
            }
            midPos = m;

            setTextContent(e, textArr.slice(0, midPos + 1).join(separator) + o.ellipsis);
            $i.children()
                .each(
                    function () {
                        $(this).toggle().toggle();
                    }
                );

            if (!test($i, o)) {
                position = midPos;
                startPos = midPos;
            } else {
                endPos = midPos;

                //	Fallback to letter
                if (o.fallbackToLetter && startPos == 0 && endPos == 0) {
                    separator = '';
                    textArr = textArr[0].split(separator);
                    position = -1;
                    midPos = -1;
                    startPos = 0;
                    endPos = textArr.length - 1;
                }
            }
        }

        if (position != -1 && !(textArr.length == 1 && textArr[0].length == 0)) {
            txt = addEllipsis(textArr.slice(0, position + 1).join(separator), o);
            setTextContent(e, txt);
        } else {
            var $w = $e.parent();
            $e.detach();

            var afterLength = (after && after.closest($w).length) ? after.length : 0;

            if ($w.contents().length > afterLength) {
                e = findLastTextNode($w.contents().eq(-1 - afterLength), $d);
            } else {
                e = findLastTextNode($w, $d, true);
                if (!afterLength) {
                    $w.detach();
                }
            }
            if (e) {
                txt = addEllipsis(getTextContent(e), o);
                setTextContent(e, txt);
                if (afterLength && after) {
                    var $parent = after.parent();

                    $(e).parent().append(after);

                    if (!$.trim($parent.html())) {
                        $parent.remove();
                    }
                }
            }
        }

        return true;
    }

    function test($i, o) {
        return $i.innerHeight() > o.maxHeight;
    }

    function addEllipsis(txt, o) {
        while ($.inArray(txt.slice(-1), o.lastCharacter.remove) > -1) {
            txt = txt.slice(0, -1);
        }
        if ($.inArray(txt.slice(-1), o.lastCharacter.noEllipsis) < 0) {
            txt += o.ellipsis;
        }
        return txt;
    }

    function getSizes($d) {
        return {
            'width': $d.innerWidth(),
            'height': $d.innerHeight()
        };
    }

    function setTextContent(e, content) {
        if (e.innerText) {
            e.innerText = content;
        } else if (e.nodeValue) {
            e.nodeValue = content;
        } else if (e.textContent) {
            e.textContent = content;
        }

    }

    function getTextContent(e) {
        if (e.innerText) {
            return e.innerText;
        } else if (e.nodeValue) {
            return e.nodeValue;
        } else if (e.textContent) {
            return e.textContent;
        } else {
            return "";
        }
    }

    function getPrevNode(n) {
        do {
            n = n.previousSibling;
        }
        while (n && n.nodeType !== 1 && n.nodeType !== 3);

        return n;
    }

    function findLastTextNode($el, $top, excludeCurrent) {
        var e = $el && $el[0], p;
        if (e) {
            if (!excludeCurrent) {
                if (e.nodeType === 3) {
                    return e;
                }
                if ($.trim($el.text())) {
                    return findLastTextNode($el.contents().last(), $top);
                }
            }
            p = getPrevNode(e);
            while (!p) {
                $el = $el.parent();
                if ($el.is($top) || !$el.length) {
                    return false;
                }
                p = getPrevNode($el[0]);
            }
            if (p) {
                return findLastTextNode($(p), $top);
            }
        }
        return false;
    }

    function getElement(e, $i) {
        if (!e) {
            return false;
        }
        if (typeof e === 'string') {
            e = $(e, $i);
            return (e.length)
                ? e
                : false;
        }
        return !e.jquery
            ? false
            : e;
    }

    function getTrueInnerHeight($el) {
        var h = $el.innerHeight(),
            a = ['paddingTop', 'paddingBottom'];

        for (var z = 0, l = a.length; z < l; z++) {
            var m = parseInt($el.css(a[z]), 10);
            if (isNaN(m)) {
                m = 0;
            }
            h -= m;
        }
        return h;
    }


    //	override jQuery.html
    var _orgHtml = $.fn.html;
    $.fn.html = function (str) {
        if (str != undef && !$.isFunction(str) && this.data('dotdotdot')) {
            return this.trigger('update', [str]);
        }
        return _orgHtml.apply(this, arguments);
    };


    //	override jQuery.text
    var _orgText = $.fn.text;
    $.fn.text = function (str) {
        if (str != undef && !$.isFunction(str) && this.data('dotdotdot')) {
            str = $('<div />').text(str).html();
            return this.trigger('update', [str]);
        }
        return _orgText.apply(this, arguments);
    };


})(jQuery);


jQuery(document).ready(function ($) {
    //We only invoke jQuery.dotdotdot on elements that have dot-ellipsis class
    $(".dot-ellipsis").each(function () {
        //Checking if update on window resize required
        var watch_window = $(this).hasClass("dot-resize-update");

        //Checking if update on timer required
        var watch_timer = $(this).hasClass("dot-timer-update");

        //Checking if height set
        var height = 0;
        var classList = $(this).attr('class').split(/\s+/);
        $.each(classList, function (index, item) {
            var matchResult = item.match(/^dot-height-(\d+)$/);
            if (matchResult !== null)
                height = Number(matchResult[1]);
        });

        //Invoking jQuery.dotdotdot
        var x = new Object();
        if (watch_timer)
            x.watch = true;
        if (watch_window)
            x.watch = 'window';
        if (height > 0)
            x.height = height;
        $(this).dotdotdot(x);
    });

});

//Updating elements (if any) on window.load event
jQuery(window).on('load', function () {
    jQuery(".dot-ellipsis.dot-load-update").trigger("update.dot");
});
